1   // Licensed under the Apache License, Version 2.0 (the "License");
2   // you may not use this file except in compliance with the License.
3   // You may obtain a copy of the License at
4   //
5   // http://www.apache.org/licenses/LICENSE-2.0
6   //
7   // Unless required by applicable law or agreed to in writing, software
8   // distributed under the License is distributed on an "AS IS" BASIS,
9   // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10  // See the License for the specific language governing permissions and
11  // limitations under the License.
12  
13  package org.apache.tapestry5.internal.services;
14  
15  import org.apache.tapestry5.Asset;
16  import org.apache.tapestry5.SymbolConstants;
17  import org.apache.tapestry5.internal.InternalConstants;
18  import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
19  import org.apache.tapestry5.ioc.IOOperation;
20  import org.apache.tapestry5.ioc.OperationTracker;
21  import org.apache.tapestry5.ioc.Resource;
22  import org.apache.tapestry5.ioc.annotations.InjectService;
23  import org.apache.tapestry5.ioc.annotations.Symbol;
24  import org.apache.tapestry5.services.AssetFactory;
25  import org.apache.tapestry5.services.Request;
26  import org.apache.tapestry5.services.Response;
27  import org.apache.tapestry5.services.assets.*;
28  
29  import javax.servlet.http.HttpServletResponse;
30  import java.io.IOException;
31  import java.io.OutputStream;
32  import java.util.Set;
33  
34  public class ResourceStreamerImpl implements ResourceStreamer
35  {
36      static final String IF_MODIFIED_SINCE_HEADER = "If-Modified-Since";
37  
38      private static final String QUOTE = "\"";
39  
40      private final Request request;
41  
42      private final Response response;
43  
44      private final StreamableResourceSource streamableResourceSource;
45  
46      private final boolean productionMode;
47  
48      private final OperationTracker tracker;
49  
50      private final ResourceChangeTracker resourceChangeTracker;
51  
52      private final String omitExpirationCacheControlHeader;
53      
54      private final AssetFactory classpathAssetFactory;
55      
56      private final AssetFactory contextAssetFactory;
57  
58      public ResourceStreamerImpl(Request request,
59  
60                                  Response response,
61  
62                                  StreamableResourceSource streamableResourceSource,
63  
64                                  OperationTracker tracker,
65  
66                                  @Symbol(SymbolConstants.PRODUCTION_MODE)
67                                  boolean productionMode,
68  
69                                  ResourceChangeTracker resourceChangeTracker,
70  
71                                  @Symbol(SymbolConstants.OMIT_EXPIRATION_CACHE_CONTROL_HEADER)
72                                  String omitExpirationCacheControlHeader,
73                                  
74                                  @InjectService("ClasspathAssetFactory")
75                                  AssetFactory classpathAssetFactory,
76                                  
77                                  @InjectService("ContextAssetFactory")
78                                  AssetFactory contextAssetFactory)
79      {
80          this.request = request;
81          this.response = response;
82          this.streamableResourceSource = streamableResourceSource;
83  
84          this.tracker = tracker;
85          this.productionMode = productionMode;
86          this.resourceChangeTracker = resourceChangeTracker;
87          this.omitExpirationCacheControlHeader = omitExpirationCacheControlHeader;
88          
89          this.classpathAssetFactory = classpathAssetFactory;
90          this.contextAssetFactory = contextAssetFactory;
91      }
92  
93      public boolean streamResource(final Resource resource, final String providedChecksum, final Set<Options> options) throws IOException
94      {
95          if (!resource.exists())
96          {
97              // TODO: Or should we just return false here and not send back a specific error with the (eventual) 404?
98  
99              response.sendError(HttpServletResponse.SC_NOT_FOUND, String.format("Unable to locate asset '%s' (the file does not exist).", resource));
100 
101             return true;
102         }
103 
104         final boolean compress = providedChecksum.startsWith("z");
105 
106         return tracker.perform("Streaming " + resource + (compress ? " (compressed)" : ""), new IOOperation<Boolean>()
107         {
108             public Boolean perform() throws IOException
109             {
110                 StreamableResourceProcessing processing = compress
111                         ? StreamableResourceProcessing.COMPRESSION_ENABLED
112                         : StreamableResourceProcessing.COMPRESSION_DISABLED;
113 
114                 StreamableResource streamable = streamableResourceSource.getStreamableResource(resource, processing, resourceChangeTracker);
115 
116                 return streamResource(resource, streamable, compress ? providedChecksum.substring(1) : providedChecksum, options);
117             }
118         });
119     }
120 
121     public boolean streamResource(StreamableResource streamable, String providedChecksum, Set<Options> options) throws IOException
122     {
123         return streamResource(null, streamable, providedChecksum, options);
124     }
125     
126     public boolean streamResource(Resource resource, StreamableResource streamable, String providedChecksum, Set<Options> options) throws IOException
127     {
128         assert streamable != null;
129         assert providedChecksum != null;
130         assert options != null;
131 
132         String actualChecksum = streamable.getChecksum();
133 
134         if (providedChecksum.length() > 0 && !providedChecksum.equals(actualChecksum))
135         {
136             
137             // TAP5-2185: Trying to find the wrongly-checksummed resource in the classpath and context,
138             // so we can create an Asset with the correct checksum and redirect to it.
139             Asset asset = null;
140             if (resource != null)
141             {
142                 asset = findAssetInsideWebapp(resource);
143             }
144             if (asset != null)
145             {
146                 response.sendRedirect(asset.toClientURL());
147                 return true;
148             }
149             return false;
150         }
151 
152 
153         // ETag should be surrounded with quotes.
154         String token = QUOTE + actualChecksum + QUOTE;
155 
156         // Even when sending a 304, we want the ETag associated with the request.
157         // In most cases (except JavaScript modules), the checksum is also embedded into the URL.
158         // However, E-Tags are also useful for enabling caching inside intermediate servers, CDNs, etc.
159         response.setHeader("ETag", token);
160 
161         // If the client can send the correct ETag token, then its cache already contains the correct
162         // content.
163         String providedToken = request.getHeader("If-None-Match");
164 
165         if (token.equals(providedToken))
166         {
167             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
168             return true;
169         }
170 
171         long lastModified = streamable.getLastModified();
172 
173         long ifModifiedSince;
174 
175         try
176         {
177             ifModifiedSince = request.getDateHeader(IF_MODIFIED_SINCE_HEADER);
178         } catch (IllegalArgumentException ex)
179         {
180             // Simulate the header being missing if it is poorly formatted.
181 
182             ifModifiedSince = -1;
183         }
184 
185         if (ifModifiedSince > 0 && ifModifiedSince >= lastModified)
186         {
187             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
188             return true;
189         }
190 
191         // Prevent the upstream code from compressing when we don't want to.
192 
193         response.disableCompression();
194 
195         response.setDateHeader("Last-Modified", lastModified);
196 
197 
198         if (productionMode && !options.contains(Options.OMIT_EXPIRATION))
199         {
200             // Starting in 5.4, this is a lot less necessary; any change to a Resource will result
201             // in a new asset URL with the changed checksum incorporated into the URL.
202             response.setDateHeader("Expires", lastModified + InternalConstants.TEN_YEARS);
203         }
204 
205         // This is really for modules, which can not have a content hash code in the URL; therefore, we want
206         // the browser to re-validate the resources on each new page render; because of the ETags, that will
207         // mostly result in quick SC_NOT_MODIFIED responses.
208         if (options.contains(Options.OMIT_EXPIRATION))
209         {
210             response.setHeader("Cache-Control", omitExpirationCacheControlHeader);
211         }
212 
213         response.setContentLength(streamable.getSize());
214 
215         if (streamable.getCompression() == CompressionStatus.COMPRESSED)
216         {
217             response.setHeader(InternalConstants.CONTENT_ENCODING_HEADER, InternalConstants.GZIP_CONTENT_ENCODING);
218         }
219 
220         ResponseCustomizer responseCustomizer = streamable.getResponseCustomizer();
221 
222         if (responseCustomizer != null)
223         {
224             responseCustomizer.customizeResponse(streamable, response);
225         }
226 
227         OutputStream os = response.getOutputStream(streamable.getContentType().toString());
228 
229         streamable.streamTo(os);
230 
231         os.close();
232 
233         return true;
234     }
235 
236     private Asset findAssetInsideWebapp(Resource resource)
237     {
238         Asset asset;
239         asset = findAssetFromClasspath(resource);
240         if (asset == null)
241         {
242             asset = findAssetFromContext(resource);
243         }
244         return asset;
245     }
246 
247     private Asset findAssetFromContext(Resource resource)
248     {
249         Asset asset = null;
250         try
251         {
252             asset = contextAssetFactory.createAsset(resource);
253         }
254         catch (RuntimeException e)
255         {
256             // not an existing context asset. go ahead.
257         }
258         return asset;
259     }
260 
261     private Asset findAssetFromClasspath(Resource resource)
262     {
263         Asset asset = null;
264         try
265         {
266             asset = classpathAssetFactory.createAsset(resource);
267         }
268         catch (RuntimeException e)
269         {
270             // not an existing classpath asset. go ahead.
271         }
272         return asset;
273     }
274 
275 }